Comparing Russell Westbrook and Oscar Robertson's Triple Double Seasons

Author: Rohan Patel

NBA player Russell Westbrook who plays for the Oklahoma City Thunder just finished an historic NBA basketball season as he became the second basketball player in NBA history to average a triple double for an entire season. A triple double entails having atleast 3 of the stat totals of points, assists, rebounds, steals, and blocks to be in double figures. A triple double is most commonly obtained through points, rebounds, and assists. During the 2017 NBA regular season, Westbrook averaged 31.6 points per game, 10.4 assists per game, and 10.7 rebounds per game.

Former NBA basketball player Oscar Robertson who used to play for the Cincinatti Royals is the only other player to average a triple double for an entire regular season as he did so 55 years ago. During the 1962 NBA regular season, Oscar Robertson averaged 30.8 points per game, 11.4 assists per game, and 12.5 rebounds per game. Many thought no one would ever average a triple double for an entire season ever again.

My project is going to compare the 2 seasons. Since it has been 55 years in between the 2 seasons, much has changed about the NBA and how basketball is played. I want to compare the differences in the way the game is played by examining their respective seasons in order to obtain a better understanding of who had the more impressive season.


In [46]:
# importing packages
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np

In [47]:
# all data is obtained through basketball-reference.com

# http://www.basketball-reference.com/teams/OKC/2017.html
# http://www.basketball-reference.com/teams/CIN/1962.html
# http://www.basketball-reference.com/leagues/NBA_stats.html

In [48]:
# all 2017 okc thunder player per game stats
okc = pd.read_csv('/Users/rohanpatel/Downloads/Per_Game_OKC_2017.csv')
okc.head()


Out[48]:
Rk Unnamed: 1 Age G GS MP FG FGA FG% 3P ... FT% ORB DRB TRB AST STL BLK TOV PF PTS/G
0 1 Russell Westbrook\westbru01 28 81 81 34.6 10.2 24.0 0.425 2.5 ... 0.845 1.7 9.0 10.7 10.4 1.6 0.4 5.4 2.3 31.6
1 2 Victor Oladipo\oladivi01 24 67 67 33.2 6.1 13.9 0.442 1.9 ... 0.753 0.6 3.8 4.3 2.6 1.2 0.3 1.8 2.3 15.9
2 3 Andre Roberson\roberan03 25 79 79 30.1 2.7 5.8 0.465 0.6 ... 0.423 1.2 3.8 5.1 1.0 1.2 1.0 0.6 2.6 6.6
3 4 Steven Adams\adamsst01 23 80 80 29.9 4.7 8.2 0.570 0.0 ... 0.611 3.5 4.2 7.7 1.1 1.1 1.0 1.8 2.4 11.3
4 5 Enes Kanter\kanteen01 24 72 0 21.3 5.6 10.2 0.545 0.1 ... 0.786 2.7 4.0 6.7 0.9 0.4 0.5 1.7 2.1 14.3

5 rows × 28 columns


In [49]:
# all 1962 cincinatti royals player per game stats
cin = pd.read_csv('/Users/rohanpatel/Downloads/Per_Game_CincRoy_1962.csv')
cin.head()


Out[49]:
Rk Player Age G GS MP FG FGA FG% 3P ... FT% ORB DRB TRB AST STL BLK TOV PF PTS
0 1 Oscar Robertson\roberos01 23 79 NaN 44.3 11.0 22.9 0.478 NaN ... 0.803 NaN NaN 12.5 11.4 NaN NaN NaN 3.3 30.8
1 2 Bucky Bockhorn\bockhbu01 28 80 NaN 38.3 6.6 15.4 0.430 NaN ... 0.789 NaN NaN 4.7 4.6 NaN NaN NaN 3.5 15.8
2 3 Jack Twyman\twymaja01 27 80 NaN 37.4 9.2 19.3 0.479 NaN ... 0.811 NaN NaN 8.0 2.7 NaN NaN NaN 4.0 22.9
3 4 Wayne Embry\embrywa01 24 75 NaN 35.0 7.5 16.1 0.466 NaN ... 0.690 NaN NaN 13.0 2.4 NaN NaN NaN 3.8 19.8
4 5 Bob Boozer\boozebo01 24 79 NaN 31.5 5.2 11.8 0.438 NaN ... 0.707 NaN NaN 10.2 1.6 NaN NaN NaN 3.5 13.7

5 rows × 28 columns


In [50]:
# only russell westbrook's points, rebounds, assists, and minutes per game
RW = okc.loc[:0]
RW = RW[['PTS/G', 'TRB', 'AST', 'MP']]
RW = RW.rename(columns={'PTS/G': 'PTS'})
RW


Out[50]:
PTS TRB AST MP
0 31.6 10.7 10.4 34.6

In [51]:
# only oscar robertson's points, rebounds, assists, and minutes per game
OR = cin.loc[:0]
OR = OR[['PTS', 'TRB', 'AST', 'MP']]
OR


Out[51]:
PTS TRB AST MP
0 30.8 12.5 11.4 44.3

In [52]:
# robertson played a considerable amount of more minutes than westbrook
# adjusting per game stats by 36 minutes played
rw_min_factor = 36/RW['MP']
or_min_factor = 36/OR['MP']

In [53]:
RW[['PTS', 'TRB', 'AST']] = RW[['PTS', 'TRB', 'AST']].apply(lambda x: x*rw_min_factor)
RW_36 = RW[['PTS', 'TRB', 'AST']]
print(RW_36)


         PTS        TRB        AST
0  32.878613  11.132948  10.820809

In [54]:
OR[['PTS', 'TRB', 'AST']] = OR[['PTS', 'TRB', 'AST']].apply(lambda x: x*or_min_factor)
OR_36 = OR[['PTS', 'TRB', 'AST']]
print(OR_36)


         PTS        TRB       AST
0  25.029345  10.158014  9.264108

In [55]:
# difference between Westbrook and Robertson's per 36 minute stats
RW_36 - OR_36


Out[55]:
PTS TRB AST
0 7.849267 0.974934 1.556701

After adjusting Westbrook and Robertson's per game stats to a per minute basis, Westbrook has the edge. He averages about 8 more points, 1 more rebound, and 1.5 more assists per 36 minutes than Robertson did during the 1962 season. Now I will look into their respective team seasons to see if there are any other adjustments that should be made when comparing the two seasons


In [56]:
# 2017 NBA stats
df_2017 = pd.read_csv('/Users/rohanpatel/Downloads/2017_NBA_Stats.csv')
df_2017
# 2017 okc thunder stats
okc_2017 = df_2017.loc[9]
okc_2017


Out[56]:
Rk                                 10
Team           Oklahoma City Thunder*
Age                              24.7
W                                  47
L                                  35
PW                                 43
PL                                 39
MOV                              0.76
SOS                              0.39
SRS                              1.14
ORtg                            108.3
DRtg                            107.5
Pace                             97.8
FTr                             0.295
3PAr                            0.295
TS%                              0.54
eFG%                              0.5
TOV%                             13.2
ORB%                               28
FT/FGA                           0.22
eFG%.1                          0.511
TOV%.1                           12.5
DRB%                               79
FT/FGA.1                        0.218
Arena         Chesapeake Energy Arena
Attendance                     746323
Name: 9, dtype: object

In [57]:
# 1962 NBA stats
df_1962 = pd.read_csv('/Users/rohanpatel/Downloads/1962_NBA_Stats.csv')
df_1962
# 1962 cincinatti royal stats
cin_1962 = df_1962.loc[4]
cin_1962


Out[57]:
Rk                             5
Team          Cincinnati Royals*
Age                           25
W                             43
L                             37
PW                            44
PL                            36
MOV                         1.76
SOS                        -0.48
SRS                         1.28
ORtg                        98.3
DRtg                        96.9
Pace                       124.9
FTr                        0.353
3PAr                         NaN
TS%                        0.506
eFG%                       0.452
TOV%                         NaN
ORB%                         NaN
FT/FGA                     0.265
eFG%.1                       NaN
TOV%.1                       NaN
DRB%                         NaN
FT/FGA.1                     NaN
Arena         Cincinnati Gardens
Attendance                   NaN
Name: 4, dtype: object

PACE

There is a noticable difference in the 'pace' stat between the 2017 Thunder and 1962 Royals. The pace stat measures how many possessions per game that a team plays per 48 minutes. The higher the pace total, the more possessions per game that the team plays. The 1962 Cincinatti Royals played about 125 possessions per game while the 2017 Oklahoma City Thunder played about 98 possessions per game. The number of possessions in a game would seem to have an impact on the stat totals of players. It would be estimated that the more possessions a team plays with, the more totals of stats such as points, rebounds, and assists would accumulate. I am going to see how the pace of teams has changed over time and how well that correlates with the number of points, rebounds, and assists that have been accumulated over time to see if Westbrook and Robertson's stats should be adjusted for the number of possessions played.


In [58]:
# nba averages per game for every season
nba_avgs = pd.read_csv('/Users/rohanpatel/Downloads/NBA_Averages_Over_Time.csv')
nba_avgs = nba_avgs[['Pace', 'PTS', 'AST', 'TRB', 'FGA']]

# pace values after the 44th row are missing 
nba_avgs = nba_avgs.iloc[:44]
print(nba_avgs)


     Pace    PTS   AST   TRB   FGA
0    96.4  105.6  22.6  43.5  85.4
1    95.8  102.7  22.3  43.8  84.6
2    93.9  100.0  22.0  43.3  83.6
3    93.9  101.0  22.0  42.7  83.0
4    92.0   98.1  22.1  42.1  82.0
5    91.3   96.3  21.0  42.2  81.4
6    92.1   99.6  21.5  41.4  81.2
7    92.7  100.4  21.2  41.7  81.7
8    91.7  100.0  21.0  41.3  80.9
9    92.4   99.9  21.8  42.0  81.5
10   91.9   98.7  21.3  41.1  79.7
11   90.5   97.0  20.6  41.0  79.0
12   90.9   97.2  21.3  41.9  80.3
13   90.1   93.4  21.3  42.2  79.8
14   91.0   95.1  21.5  42.3  80.8
15   90.7   95.5  21.9  42.4  81.3
16   91.3   94.8  21.8  42.5  80.6
17   93.1   97.5  22.3  42.9  82.1
18   88.9   91.6  20.7  41.7  78.2
19   90.3   95.6  22.0  41.5  79.7
20   90.1   96.9  22.0  41.1  79.3
21   91.8   99.5  22.7  41.3  80.2
22   92.9  101.4  23.4  41.6  81.5
23   95.1  101.5  24.4  43.0  84.4
24   96.8  105.3  24.7  43.1  86.0
25   96.6  105.3  24.5  43.7  87.3
26   97.8  106.3  24.7  43.3  87.2
27   98.3  107.0  24.9  43.1  87.2
28  100.6  109.2  25.6  43.9  89.0
29   99.6  108.2  25.8  43.4  87.7
30  100.8  109.9  26.0  44.0  88.8
31  102.1  110.2  26.0  43.6  88.6
32  102.1  110.8  26.3  43.5  89.1
33  101.4  110.1  26.2  43.0  88.4
34  103.1  108.5  25.9  44.5  89.7
35  100.9  108.6  25.2  43.5  88.2
36  101.8  108.1  25.5  43.5  88.4
37  103.1  109.3  25.8  44.9  90.6
38  105.8  110.3  25.8  45.2  91.7
39  106.7  108.5  25.0  47.1  92.9
40  106.5  106.5  23.9  47.1  92.0
41  105.5  104.3  23.0  47.4  91.7
42  104.5  102.6  23.8  47.1  91.1
43  107.8  105.7  24.6  48.2  93.9

In [59]:
# scatterplots of stats against number of possessions
fig, ax = plt.subplots(nrows = 4, ncols = 1, sharex = True, figsize=(10, 20))

ax[0].scatter(nba_avgs['Pace'], nba_avgs['PTS'], color = 'green')
ax[1].scatter(nba_avgs['Pace'], nba_avgs['TRB'], color = 'blue')
ax[2].scatter(nba_avgs['Pace'], nba_avgs['AST'], color = 'red')
ax[3].scatter(nba_avgs['Pace'], nba_avgs['FGA'], color = 'orange')

ax[0].set_ylabel('POINTS', fontsize = 18)
ax[1].set_ylabel('REBOUNDS', fontsize = 18)
ax[2].set_ylabel('ASSISTS', fontsize = 18)
ax[3].set_ylabel('SHOT ATTEMPTS', fontsize = 18)
ax[3].set_xlabel('NUMBER OF POSSESSIONS', fontsize = 18)

plt.suptitle('STAT TOTALS VS NUMBER OF POSSESSIONS (PER GAME)', fontsize = 22)

plt.show()


It seems pretty clear that the more possessions that a team plays with, the more stat totals they will accumulate. Pace seems to predict the number of shot attempts and rebounds very well just by looking at the scatterplots. Assists and points also increase as pace increases, but it seems to dip off towards the higher paces. Robertson played with a very high pace. I will perform a linear regression for assists and points.


In [60]:
import statsmodels.api as sm
from pandas.tools.plotting import scatter_matrix

y = np.matrix(nba_avgs['PTS']).transpose()
x1 = np.matrix(nba_avgs['Pace']).transpose()
X = sm.add_constant(x1)
model = sm.OLS(y,X)
f = model.fit()
print(f.summary())


                            OLS Regression Results                            
==============================================================================
Dep. Variable:                      y   R-squared:                       0.738
Model:                            OLS   Adj. R-squared:                  0.732
Method:                 Least Squares   F-statistic:                     118.4
Date:                Thu, 11 May 2017   Prob (F-statistic):           8.52e-14
Time:                        14:06:54   Log-Likelihood:                -107.44
No. Observations:                  44   AIC:                             218.9
Df Residuals:                      42   BIC:                             222.4
Df Model:                           1                                         
Covariance Type:            nonrobust                                         
==============================================================================
                 coef    std err          t      P>|t|      [95.0% Conf. Int.]
------------------------------------------------------------------------------
const         21.8499      7.433      2.939      0.005         6.849    36.851
x1             0.8354      0.077     10.880      0.000         0.680     0.990
==============================================================================
Omnibus:                        3.731   Durbin-Watson:                   0.267
Prob(Omnibus):                  0.155   Jarque-Bera (JB):                3.537
Skew:                          -0.667   Prob(JB):                        0.171
Kurtosis:                       2.612   Cond. No.                     1.68e+03
==============================================================================

Warnings:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
[2] The condition number is large, 1.68e+03. This might indicate that there are
strong multicollinearity or other numerical problems.

P-Value shows that pace is a statistically significant predictor of points and R-squared shows that about 74% of the variation in points comes from the variation in possessions which is pretty significant. It makes sense to adjust Robertson and Westbrook's points for pace


In [61]:
y = np.matrix(nba_avgs['AST']).transpose()
x1 = np.matrix(nba_avgs['Pace']).transpose()
X = sm.add_constant(x1)
model = sm.OLS(y,X)
f = model.fit()
print(f.summary())


                            OLS Regression Results                            
==============================================================================
Dep. Variable:                      y   R-squared:                       0.660
Model:                            OLS   Adj. R-squared:                  0.652
Method:                 Least Squares   F-statistic:                     81.71
Date:                Thu, 11 May 2017   Prob (F-statistic):           2.09e-11
Time:                        14:06:59   Log-Likelihood:                -65.726
No. Observations:                  44   AIC:                             135.5
Df Residuals:                      42   BIC:                             139.0
Df Model:                           1                                         
Covariance Type:            nonrobust                                         
==============================================================================
                 coef    std err          t      P>|t|      [95.0% Conf. Int.]
------------------------------------------------------------------------------
const         -2.6316      2.880     -0.914      0.366        -8.444     3.181
x1             0.2689      0.030      9.039      0.000         0.209     0.329
==============================================================================
Omnibus:                        1.501   Durbin-Watson:                   0.190
Prob(Omnibus):                  0.472   Jarque-Bera (JB):                1.414
Skew:                          -0.325   Prob(JB):                        0.493
Kurtosis:                       2.410   Cond. No.                     1.68e+03
==============================================================================

Warnings:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
[2] The condition number is large, 1.68e+03. This might indicate that there are
strong multicollinearity or other numerical problems.

Possessions also significant in predicting the number of assists


In [62]:
# adjusting both player's per 36 minute points, rebounds, and assists per 100 team possessions

rw_pace_factor = 100/okc_2017['Pace']
or_pace_factor = 100/cin_1962['Pace']

In [63]:
RW_36_100 = RW_36.apply(lambda x: x*rw_pace_factor)
print(RW_36_100)


         PTS        TRB        AST
0  33.618213  11.383382  11.064222

In [64]:
OR_36_100 = OR_36.apply(lambda x: x*or_pace_factor)
print(OR_36_100)


         PTS       TRB      AST
0  20.039508  8.132917  7.41722

In [65]:
print(RW_36_100 - OR_36_100)


         PTS       TRB       AST
0  13.578706  3.250465  3.647002

In [66]:
# westbrook's  per 36 minute stats adjusted for 1962 Cincinatti Royals pace

RW_36_1962 = RW_36 * (cin_1962['Pace']/okc_2017['Pace'])
print(RW_36_1962)


         PTS        TRB        AST
0  41.989149  14.217845  13.819213

In [67]:
# robertson's per 36 minute stats adjusted for 2017 OKC Thunder Pace

OR_36_2017 = OR_36 * (okc_2017['Pace']/cin_1962['Pace'])
print(OR_36_2017)


         PTS       TRB       AST
0  19.598639  7.953993  7.254042

In [68]:
# difference between the two if westbrook played at 1962 robertson's pace per 36 minutes
print(RW_36_1962 - OR_36)


         PTS       TRB       AST
0  16.959803  4.059831  4.555105

In [69]:
# difference between the two if robertson played at 2017 westbrook's pace per 36 minutes
print(RW_36 - OR_36_2017)


         PTS       TRB       AST
0  13.279974  3.178955  3.566768

In [70]:
# huge advantages for westbrook after adjusting for possessions

CONCLUSION

PACE MATTERS. Pace is something that almost never gets mentioned in any basketball debate when comparing across eras. Per-game statistics are what is mainly used when comparing players to see who is better. But as we saw, the number of possessions a player plays with varies largely between teams and plays a major factor in the amount of total statistics he is able to accumulate. Pace has steadily slowed down as the NBA has gotten older other than the slight recent surge the past few years. In Robertson's time, a team playing with 130 possessions per game was not unusual, but 130 possessions per game today is unheard of. It now does not seem like a coincidence that 1962 was also the the season that Wilt Chamberlain mythically averaged 50 points per game and scored a 100 points in a single game

This goes to show how impressive Westbrook's 2017 NBA season actually was. Many recognize the greatness in that it's only the 2nd time ever someone has averaged a triple double for an entire season. But people do not realize how much better it was than Robertson's season. As most people do, just glancing at the per game statistics makes the seasons look similar and might even give Robertson an edge. But as we broke down the statistics and adjusted for pace and minutes played, Westbrook averaged about 13.5 more points, 3.25 more rebounds, and 3.65 more assists than Robertson did per 36 minutes and 100 possessions. Robertson's averages of about 20 points, 8 rebounds, and 7.5 assists per game after adjusting for pace and minutes is about what you'd expect from a regular all-star player today. The numbers are still very good, but not what they seem by just looking at the box score. The typical NBA box-score is limited because it does not take into account many factors that lead to the accumulation of statistics as per-game numbers can be very misleading.